﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interactivity;
using ChessKit.Common;
using Microsoft.Expression.Interactivity.Layout;
using Vector = System.Windows.Vector;

namespace ChessKit.Ui
{
  /// <summary>
  /// Repositions the attached element visual children in response to mouse drag gestures.
  /// </summary>
  /// <remarks><para>This behavior is similar to the basic system
  ///   <see cref="MouseDragElementBehavior"/> but has a few differences:
  /// <list type="bullet">
  /// <item>Affects all visual children of the associated element</item>
  /// <item>Drop point is center of the element being dragged, 
  ///   and not merely the mouse position</item>
  /// <item>There's the <see cref="DraggingEvent"/> you can use to cancel dragging</item>
  /// <item>There's the <see cref="DragEvent"/> that allows you to set
  ///   <see cref="DragEventArgs.FailToDrop"/> action</item>
  /// <item>There is no <see cref="MouseDragElementBehavior.Dragging"/> event
  ///   one could use to track the progress of the dragging</item>
  /// <item><see cref="AllowDragProperty"/>, and <see cref="AllowDropProperty"/>
  ///   allow you to shape sets of objects that can be dragged and drop targets</item>
  /// </list> </para>
  /// <para>While being dragged element is put into the adorner layer
  /// </para></remarks>
  public class DragDropBehavior : Behavior<UIElement>
  {
    private Control _target;
    private Point _relativePosition;
    private DragAdorner _adorner;
    private Action _failToDrop;
    private Vector _basePoint;
    private Vector _correction;

    #region ' Dragging (routed event) '

    public static readonly RoutedEvent DraggingEvent = EventManager.RegisterRoutedEvent("Dragging",
      RoutingStrategy.Bubble, typeof(EventHandler<DraggingEventArgs>), typeof(DragDropBehavior));

    public static void AddDraggingHandler(UIElement uie, EventHandler<DraggingEventArgs> handler)
    {
      uie.AddHandler(DraggingEvent, handler);
    }
    public static void RemoveDraggingHandler(UIElement uie, EventHandler<DraggingEventArgs> handler)
    {
      uie.RemoveHandler(DraggingEvent, handler);
    }
    
    private bool RaiseDraggingEvent()
    {
      var eventArgs = new DraggingEventArgs(DraggingEvent);
      _target.RaiseEvent(eventArgs);
      return eventArgs.CanDrag;
    }

    #endregion

    #region ' Drag (routed event) '

    public static readonly RoutedEvent DragEvent = EventManager.RegisterRoutedEvent("Drag",
      RoutingStrategy.Bubble, typeof(EventHandler<DragEventArgs>), typeof(DragDropBehavior));

    public static void AddDragHandler(UIElement uie, EventHandler<DragEventArgs> handler)
    {
      uie.AddHandler(DragEvent, handler);
    }
    public static void RemoveDragHandler(UIElement uie, EventHandler<DragEventArgs> handler)
    {
      uie.RemoveHandler(DragEvent, handler);
    }
    
    private Action RaiseDragEvent()
    {
      var eventArgs = new DragEventArgs(DragEvent);
      _target.RaiseEvent(eventArgs);
      return eventArgs.FailToDrop;
    }

    #endregion

    #region ' Drop (routed event) '

    public static readonly RoutedEvent DropEvent = EventManager.RegisterRoutedEvent("Drop", 
      RoutingStrategy.Bubble, typeof(EventHandler<DropEventArgs>), typeof(DragDropBehavior));

    public static void AddDropHandler(UIElement uie, EventHandler<DropEventArgs> handler)
    {
      uie.AddHandler(DropEvent, handler);
    }
    public static void RemoveDropHandler(UIElement uie, EventHandler<DropEventArgs> handler)
    {
      uie.RemoveHandler(DropEvent, handler);
    }
    
    private void RaiseDropEvent(UIElement dropTarget)
    {
      dropTarget.RaiseEvent(new DropEventArgs(DropEvent, _target));
    }

    #endregion

    #region ' AllowDrop : bool (Attached Property) '

    public static readonly DependencyProperty AllowDropProperty =
      DependencyProperty.RegisterAttached("AllowDrop",
      typeof(bool), typeof(DragDropBehavior),
      new FrameworkPropertyMetadata(default(bool),
        FrameworkPropertyMetadataOptions.AffectsParentMeasure));

    public static bool GetAllowDrop(DependencyObject obj)
    {
      return (bool)obj.GetValue(AllowDropProperty);
    }

    public static void SetAllowDrop(DependencyObject obj, bool value)
    {
      obj.SetValue(AllowDropProperty, value);
    }

    #endregion

    #region ' AllowDrag : bool (Attached Property) '

    public static readonly DependencyProperty AllowDragProperty =
      DependencyProperty.RegisterAttached("AllowDrag",
      typeof(bool), typeof(DragDropBehavior),
      new FrameworkPropertyMetadata(false,
        FrameworkPropertyMetadataOptions.AffectsParentMeasure));

    public static bool GetAllowDrag(DependencyObject obj)
    {
      return (bool)obj.GetValue(AllowDragProperty);
    }

    public static void SetAllowDrag(DependencyObject obj, bool value)
    {
      obj.SetValue(AllowDragProperty, value);
    }

    #endregion

    #region ' HoverTemplate : ControlTemplate (dependency property) '

    public static readonly DependencyProperty HoverTemplateProperty =
      DependencyProperty.Register("HoverTemplate", typeof (ControlTemplate),
      typeof (DragDropBehavior), new PropertyMetadata(default(ControlTemplate)));

    public ControlTemplate HoverTemplate
    {
      get { return (ControlTemplate) GetValue(HoverTemplateProperty); }
      set { SetValue(HoverTemplateProperty, value); }
    }

    #endregion

    protected override void OnAttached()
    {
      AssociatedObject.AddHandler(
        UIElement.MouseLeftButtonDownEvent, 
        new MouseButtonEventHandler(OnMouseLeftButtonDown));
    }

    protected override void OnDetaching()
    {
      AssociatedObject.RemoveHandler(
        UIElement.MouseLeftButtonDownEvent,
        new MouseButtonEventHandler(OnMouseLeftButtonDown));
    }

    private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
      var o = e.OriginalSource as DependencyObject;
      _target = o.FindVisualParent<Control>(GetAllowDrag);
      if (_target == null) return;
      if (!RaiseDraggingEvent()) return;

      var center = (Vector)_target.RenderSize / 2;
      _correction = (Vector)e.GetPosition(_target) - center;

      StartDrag(e.GetPosition(AssociatedObject));
    }

    private void StartDrag(Point positionInElementCoordinates)
    {
      _adorner = CreateAdorner();
      _relativePosition = positionInElementCoordinates;
      AssociatedObject.CaptureMouse();
      AssociatedObject.MouseMove += OnMouseMove;
      AssociatedObject.LostMouseCapture += OnLostMouseCapture;
      AssociatedObject.AddHandler(UIElement.MouseLeftButtonUpEvent, 
        new MouseButtonEventHandler(OnMouseLeftButtonUp));
    }

    private void OnLostMouseCapture(object sender, MouseEventArgs e)
    {
      var dropTarget = GetDropTarget(e);
      EndDrag();
      
      // just for the case _failToDrop throws exception
      var failToDrop = _failToDrop;
      _failToDrop = null;

      if (dropTarget == null) failToDrop();
      else RaiseDropEvent(dropTarget);

      _target = null;
    }

    private UIElement GetDropTarget(MouseEventArgs e)
    {
      var position = e.GetPosition(AssociatedObject) - _correction;
      return AssociatedObject.HitTest<UIElement>(position, GetAllowDrop);
    }

    private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
      AssociatedObject.ReleaseMouseCapture();
    }

    private void OnMouseMove(object sender, MouseEventArgs e)
    {
      HandleDrag(e.GetPosition(AssociatedObject));
    }

    private void HandleDrag(Point newPositionInElementCoordinates)
    {
      _adorner.Offset = _basePoint + 
        newPositionInElementCoordinates - _relativePosition;
    }

    private DragAdorner CreateAdorner()
    {
      var template = HoverTemplate ?? _target.Template;
      var originalSize = _target.RenderSize;
      var adornerLayer = AdornerLayer.GetAdornerLayer(AssociatedObject);
      _basePoint = (Vector)_target.TranslatePoint(new Point(), AssociatedObject);

      _failToDrop = RaiseDragEvent();

      if (_target.Parent != null)
        throw new InvalidOperationException(
          "Please detach dragged element from visual tree on Drag event");

      var adorner = new DragAdorner(AssociatedObject, _target, originalSize)
                      {
                        Offset = _basePoint 
                      };

      _target.SetCurrentValue(Control.TemplateProperty, template);

      adornerLayer.Add(adorner);
      return adorner;
    }

    private void EndDrag()
    {
      AssociatedObject.MouseMove -= OnMouseMove;
      AssociatedObject.LostMouseCapture -= OnLostMouseCapture;
      AssociatedObject.RemoveHandler(
        UIElement.MouseLeftButtonUpEvent,
        new MouseButtonEventHandler(OnMouseLeftButtonUp));
      _target.SetCurrentValue(
        Control.TemplateProperty, DependencyProperty.UnsetValue);
      _adorner.Dispose();
    }
  }
}
